import asyncio
import os
from typing import List
from uuid import UUID

from dotenv import load_dotenv
from pydantic import UUID4
from starlette.responses import JSONResponse

import exchange_main

load_dotenv()  # take environment variables from .env.

import logging
import uvicorn
from fastapi import Depends
from fastapi import FastAPI, status, HTTPException, BackgroundTasks
from fastapi_pagination import add_pagination, Page
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import select, desc
from sqlalchemy.orm import Session
from starlette.middleware.cors import CORSMiddleware

import crud
import models
import schemas
from database import engine, get_db
from deps import get_current_user, RoleChecker
from models import Role, Status
from schemas import UserOut, UserAuth, UserRegistration, OrganizationCreate, OrganizationOut, RespOut, RespIn, \
    OrganizationAutocomplete, PasswordRecovery, NewPassword
from utils import verify_password, create_access_token
from verification_email_sender import anyway_send_email_to, anyway_recover_password
from exchange_main import send_to

models.Base.metadata.create_all(bind=engine)
app = FastAPI()

allow_origins = os.environ['ALLOW_ORIGINS'].split(',')
app.add_middleware(
    CORSMiddleware,
    allow_origins=allow_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

user_role = RoleChecker([Role.User, Role.Admin, Role.Alexander])
admin_role = RoleChecker([Role.Admin, Role.Alexander])
alexandr_role = RoleChecker([Role.Alexander])


@app.on_event('startup')
async def app_startup():
    asyncio.create_task(exchange_main.main())


@app.get("/api/req/all", response_model=Page[schemas.ReqShort])
async def get_user_all_req(user: models.User = Depends(get_current_user),
                           db: Session = Depends(get_db),
                           sort: str | None = None,
                           status: models.Status | None = None,
                           topic: str | None = None,
                           description: str | None = None,
                           user_id: UUID | None = None):
    query = select(models.Request).join(models.StatusWithTime).where(models.StatusWithTime.is_current == True)
    if user.role == Role.User:
        query = query.filter(models.Request.user_id == user.id)
    elif user_id:
        query = query.filter(models.Request.user_id == user_id)
    if sort:
        if sort == "desc":
            query = query.order_by(desc(models.Request.id))
        elif sort == "asc":
            query = query.order_by(models.Request.id)
    if status:
        query = query.filter_by(is_current=True).filter(models.StatusWithTime.status == status)
    if topic:
        query = query.filter(models.Request.topic.icontains(topic))
    if description:
        query = query.filter(models.Request.description.icontains(description))
    return paginate(db, query)


@app.get("/api/req_full/all", response_model=Page[schemas.Req])
async def get_user_all_req_full(user: models.User = Depends(get_current_user),
                                db: Session = Depends(get_db),
                                sort: str | None = None,
                                status: models.Status | None = None,
                                topic: str | None = None,
                                description: str | None = None,
                                user_id: UUID | None = None):
    query = select(models.Request).join(models.StatusWithTime).where(models.StatusWithTime.is_current == True)
    if user.role == Role.User:
        query = query.filter(models.Request.user_id == user.id)
    elif user_id:
        query = query.filter(models.Request.user_id == user_id)
    if sort:
        if sort == "desc":
            query = query.order_by(desc(models.Request.id))
        elif sort == "asc":
            query = query.order_by(models.Request.id)
    if status:
        query = query.filter_by(is_current=True).filter(models.StatusWithTime.status == status)
    if topic:
        query = query.filter(models.Request.topic.icontains(topic))
    if description:
        query = query.filter(models.Request.description.icontains(description))
    return paginate(db, query)


@app.post("/api/req/create", response_model=schemas.ReqShort)
async def create_req(req: schemas.ReqCreate, db: Session = Depends(get_db),
                     user: models.User = Depends(get_current_user)):
    return crud.create_req(db, req, user.id)


@app.put("/api/req/{req_id}/{status}", status_code=status.HTTP_200_OK)
async def change_status(req_id: int, status: Status, db: Session = Depends(get_db),
                        user: models.User = Depends(get_current_user)):
    req = crud.get_req(db, req_id)
    if req is None:
        raise HTTPException(status_code=404, detail="Данного запроса не существует")
    if user.role == Role.User and req.user_id != user.id:
        raise HTTPException(status_code=403, detail="Вы не можете редактировать чужие запросы")
    if req.current_status.status != status:
        if user.role == Role.User and status != Status.Closed:
            raise HTTPException(status_code=403, detail="Вы можете только закрывать запросы")
        cur_stat = req.current_status
        cur_stat.is_current = False
        req.statuses.append(status.with_time())
        db.add(cur_stat)
        db.add(req)
        db.commit()
        db.refresh(req)
        return req.current_status
    else:
        raise HTTPException(status_code=400, detail="Этот статус уже установлен")


@app.get('/api/req/{req_id}', response_model=schemas.Req)
async def get_req(req_id: int, user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
    req = crud.get_req(db, req_id)
    if req is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Req with this id not exist"
        )
    if user.role == Role.User and req.user_id != user.id:
        raise HTTPException(status_code=403, detail="Операция не позволена!")
    return req


@app.get('/api/organization/autocomplete', response_model=List[OrganizationAutocomplete])
async def autocomplete(name: str, db: Session = Depends(get_db)):
    return db.query(models.Organization.name).filter(models.Organization.name.icontains(name)).limit(5)


@app.post('/api/organization/create', dependencies=[Depends(admin_role)], response_model=OrganizationOut)
async def create_organization(data: OrganizationCreate,
                              db: Session = Depends(get_db)):
    return crud.create_organization(db, data)


@app.get('/api/organization/all', dependencies=[Depends(admin_role)], response_model=Page[OrganizationOut])
async def all_organizations(name: str | None = None, db: Session = Depends(get_db)):
    query = select(models.Organization)
    if name:
        query = query.filter(models.Organization.name.icontains(name))
    return paginate(db, query)


@app.get('/api/organization/{org_id}', dependencies=[Depends(admin_role)], response_model=OrganizationOut)
async def get_organization(org_id: int, db: Session = Depends(get_db)):
    org = crud.get_organization(db, org_id)
    if org is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Организации с данным id не существует"
        )
    return org


@app.post('/api/signup', summary="Create new user")
async def signup(data: UserRegistration, background_tasks: BackgroundTasks,
                 db: Session = Depends(get_db)):
    # querying database to check if user already exist
    user = crud.get_user_by_name(db, data.name)
    if user is not None:
        return JSONResponse(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            content={
                "detail": [
                    {
                        "type": "invalid_value",
                        "loc": ["body", "name"],
                        "msg": "Пользователесь с таким именем уже существует",
                    },
                ]
            }
        )
    user = crud.get_user_by_email(db, data.email)
    if user is not None:
        return JSONResponse(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            content={
                "detail": [
                    {
                        "type": "invalid_value",
                        "loc": ["body", "email"],
                        "msg": "Пользователесь с таким email уже существует",
                    },
                ]
            }
        )
    organization = crud.get_organization_by_name_or_inn(db, data.organization)
    if organization is None:
        return JSONResponse(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            content={
                "detail": [
                    {
                        "type": "invalid_value",
                        "loc": ["body", "organization"],
                        "msg": "Такой организации мы не обслуживаем",
                    },
                ]
            }
        )
    unconfirmed_user = crud.create_unconfirmed_user(db, data, organization.id)
    background_tasks.add_task(anyway_send_email_to, unconfirmed_user.email, unconfirmed_user.name, unconfirmed_user.id)


@app.post('/api/login', summary="Create access and refresh tokens for user", response_model=schemas.UserWithTokens)
async def login2(user_auth: UserAuth, db: Session = Depends(get_db)):
    user = crud.get_user_by_name_or_email(db, user_auth.login)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Неправильный логин или пароль"
        )
    hashed_pass = user.password
    if not verify_password(user_auth.password, hashed_pass):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Неправильный логин или пароль"
        )
    return {
        "user": user,
        "tokens": {
            "access_token": create_access_token(user.name),
        }
    }


@app.get('/api/users/me', summary='Get details of currently logged in user', response_model=UserOut)
async def get_me(user: models.User = Depends(get_current_user)):
    return user


@app.get('/api/users/all', dependencies=[Depends(admin_role)], response_model=Page[UserOut])
async def get_users(db: Session = Depends(get_db), organization_id: int | None = None,
                    sort: str | None = None,
                    name: str | None = None,
                    email: str | None = None,
                    organization: str | None = None,
                    inn: str | None = None,
                    role: Role | None = None):
    query = select(models.User).join(models.Organization)
    if sort:
        if sort == 'asc':
            query = query.order_by(models.User.id)
        elif sort == 'desc':
            query = query.order_by(desc(models.User.id))
    if organization:
        query = query.filter(models.Organization.name.icontains(organization))
    if inn:
        query = query.filter(models.Organization.inn.icontains(inn))
    if organization_id:
        query = query.filter(models.User.organization_id == organization_id)
    if name:
        query = query.filter(models.User.name.icontains(name))
    if email:
        query = query.filter(models.User.email.icontains(email))
    if role:
        query = query.filter(models.User.role == role)
    return paginate(db, query)


@app.get('/api/users/{user_id}', dependencies=[Depends(admin_role)], response_model=UserOut)
async def get_user(user_id: UUID, db: Session = Depends(get_db)):
    ans = crud.get_user(db, user_id)
    if ans is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Пользователь не найден"
        )
    return ans


@app.put('/api/users/{user_id}/{role}', dependencies=[Depends(admin_role)], response_model=UserOut)
async def change_role(user_id: UUID, role: Role, db: Session = Depends(get_db)):
    user = crud.get_user(db, user_id)
    user.role = role
    db.add(user)
    db.commit()
    db.refresh(user)
    return user


@app.get('/api/resp/all', dependencies=[Depends(admin_role)], response_model=Page[RespOut])
async def get_all_resp(db: Session = Depends(get_db),
                       sort: str | None = None,
                       description: str | None = None,
                       user_id: UUID | None = None):
    query = select(models.Response)
    if user_id:
        query = query.filter(models.Response.user_id == user_id)
    if sort:
        if sort == "desc":
            query = query.order_by(desc(models.Response.id))
        elif sort == "asc":
            query = query.order_by(models.Response.id)
    if description:
        query = query.filter(models.Response.description.icontains(description))
    return paginate(db, query)


@app.post('/api/resp/{req_id}', dependencies=[Depends(admin_role)], response_model=RespOut)
async def send_resp(req_id: int, resp_in: RespIn, background_tasks: BackgroundTasks,
                    user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
    resp = crud.create_resp(db, req_id, resp_in, user.id)
    background_tasks.add_task(send_to, resp.request.user.email,
                              'Ответ на запрос: {}'.format(resp.request.topic), resp.description)
    return resp


@app.get('/api/resp/{req_id}', response_model=RespOut)
async def get_resp(req_id: int, user: models.User = Depends(get_current_user),
                   db: Session = Depends(get_db)):
    ans: models.Response = crud.get_resp_by_req_id(db, req_id)
    if ans is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Пользователь не найден"
        )
    if user.role == Role.User and ans.request.user_id != user.id:
        raise HTTPException(status_code=403, detail="Операция не позволена!")
    return ans


@app.get('/api/verification/{uuid}', response_model=schemas.UserWithTokens)
async def verification_user(uuid: UUID4, db: Session = Depends(get_db)):
    user = crud.get_unconfirmed_user(db, uuid)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Не пралильный url"
        )
    user = crud.confirm_user(db, user)
    return {
        "user": user,
        "tokens": {
            "access_token": create_access_token(user.name),
        }
    }


@app.post('/api/recovery')
async def password_recovery(pass_recov: PasswordRecovery, background_tasks: BackgroundTasks,
                            db: Session = Depends(get_db)):
    user = crud.get_user_by_email(db, pass_recov.email)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Пользователя не существует"
        )
    code = crud.create_random_code(db, user)
    background_tasks.add_task(anyway_recover_password, pass_recov.email, user.name, code.id)


@app.post('/api/recovery/{code}')
async def new_pass(code: UUID, new_pass: NewPassword, db: Session = Depends(get_db)):
    random_code = crud.get_code(db, code)
    if random_code is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Не пралильный url"
        )
    user = random_code.user
    crud.change_user_password(db, user, new_pass.password)
    db.delete(random_code)
    db.commit()


add_pagination(app)


def main():
    log_level = os.environ['LOG_LEVEL']
    if os.environ['PRODUCTION'] == 'true':
        logging.basicConfig(level=logging.ERROR)
        uvicorn.run("main:app", host="0.0.0.0", reload=False,
                    port=8000, log_level=log_level,
                    ssl_keyfile="privatekey.pem",
                    ssl_certfile="certificate.pem")
    else:
        logging.basicConfig(level=logging.DEBUG)
        uvicorn.run("main:app", host="0.0.0.0", reload=True,
                    port=8000, reload_excludes=["venv", "__pycache__", ".idea"],
                    log_level=log_level)


if __name__ == "__main__":
    main()
